#if defined(_WIN32)

#include    "watcher.h"
#include    "logger.h"

#include    <Psapi.h>
#include	<io.h>
#include	<vector>

static int ProcessorNumber = 1;

static LONGLONG FileTimeToInteger(const FILETIME & ft) {
	LARGE_INTEGER ret;
	ret.HighPart = ft.dwHighDateTime;
	ret.LowPart = ft.dwLowDateTime;
	return ret.QuadPart;
}

Watcher & Watcher::Instance() {
	static std::unique_ptr<Watcher> iIns;
	if (!iIns) {
		iIns.reset(new Watcher);

		SYSTEM_INFO si;
		::GetSystemInfo(&si);
		ProcessorNumber = (int)si.dwNumberOfProcessors;
	}

	return *iIns;
}

void Watcher::Breath() {
    static time_t nLastBreath = 0;

	time_t nTick = time(NULL);
	if (nTick == nLastBreath) return;
	nLastBreath = nTick;

	Json::Value iMsg;
	iMsg["action"]		= "update";
	iMsg["watchers"]	= Json::Value(Json::arrayValue);	

	for (auto & kv : _mWatcher) {
		if (kv.second.emStatus != EStatus::Running) continue;
		Tail(kv.first, kv.second);

        HANDLE hProc = ::OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_QUERY_LIMITED_INFORMATION|PROCESS_VM_READ, false, kv.second.nPid);
		if (hProc == INVALID_HANDLE_VALUE) {
			CatchExit(kv.first, kv.second, EStatus::Fatal);
			continue;
		}

		DWORD nExitCode = 0;
		::GetExitCodeProcess(hProc, &nExitCode);
		if (nExitCode != STILL_ACTIVE) {
			CatchExit(kv.first, kv.second, nExitCode == 0 ? EStatus::Stopped : EStatus::Fatal);
			continue;
		}
		
		PROCESS_MEMORY_COUNTERS pmc;
		ZeroMemory(&pmc, sizeof(PROCESS_MEMORY_COUNTERS));
		pmc.cb = sizeof(PROCESS_MEMORY_COUNTERS);

		Json::Value iState;
		iState["name"] = kv.first;
		iState["vsz"] = "-";

		::GetProcessMemoryInfo(hProc, &pmc, sizeof(PROCESS_MEMORY_COUNTERS));
		iState["res"] = (int)pmc.PagefileUsage / 1024;

		FILETIME ftCreation, ftExit, ftKernel, ftUser, ftNow;
		::GetSystemTimeAsFileTime(&ftNow);

		LONGLONG nNow = FileTimeToInteger(ftNow);
		LONGLONG nPeriod = nNow - kv.second.nLastCalcCPU;
		kv.second.nLastCalcCPU = nNow;

		if (::GetProcessTimes(hProc, &ftCreation, &ftExit, &ftKernel, &ftUser)) {
			LONGLONG nKernel = FileTimeToInteger(ftKernel);
			LONGLONG nUser = FileTimeToInteger(ftUser);
			LONGLONG nTotal = nKernel + nUser;
			LONGLONG nDiff = nTotal - kv.second.nLastCPUTotal;
			kv.second.nLastCPUTotal = nTotal;
			iState["cpu"] = std::to_string(nDiff * 1.f / nPeriod);
		} else {
			iState["cpu"] = "-";
		}

		::CloseHandle(hProc);
		iMsg["watchers"].append(iState);
    }

    if (iMsg["watchers"].size() > 0) Notify("/ws", iMsg);
}

void Watcher::Start(const std::string & sName, const std::string & sPath, const std::string &sCmd, int nRetry) {
    auto it = _mWatcher.find(sName);
	Info * pInfo = nullptr;

	if (it != _mWatcher.end() && it->second.emStatus == EStatus::Running) return;
	if (it == _mWatcher.end()) {
		Info iInfo;
		iInfo.nRetry	= nRetry;
		iInfo.sCmd		= sCmd;
		iInfo.sPath		= sPath;

		_mWatcher[sName] = iInfo;
		pInfo = &_mWatcher[sName];
	} else if (it->second.emStatus == EStatus::Running) {
		return;
	} else {
		pInfo = &it->second;
	}

    PROCESS_INFORMATION pi;
    STARTUPINFOA si;
    SECURITY_ATTRIBUTES sa;

    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
    ZeroMemory(&si, sizeof(STARTUPINFOA));
    
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle = TRUE;
    sa.lpSecurityDescriptor = NULL;

	HANDLE hReader, hWriter;
	if (!::CreatePipe(&hReader, &hWriter, &sa, 0)) {
        LOG_ERR("Failed to create pipe for start watcher [%s]. Error: %d", sName.c_str(), ::GetLastError());
        return;
    }

    si.cb = sizeof(STARTUPINFOA);
    si.hStdError = hWriter;
    si.hStdOutput = hWriter;
    si.dwFlags |= STARTF_USESTDHANDLES;

	char pCmd[512] = {0};
	char pWorkDir[512] = {0};

	if (!sPath.empty()) {
		if (sPath[0] == '.') {
			::GetFullPathNameA(sPath.c_str(), 512, pWorkDir, NULL);
		} else {
			strncpy(pWorkDir, sPath.c_str(), 512);
		}
	}

	if (sCmd[0] == '.') {
		std::string sExec;
		sExec.append(pWorkDir);
		sExec.append("\\");
		sExec.append(sCmd);

		std::vector<std::string> vSupportedSuffix;
		vSupportedSuffix.push_back(".exe");
		vSupportedSuffix.push_back(".bat");
		vSupportedSuffix.push_back(".cmd");

		bool bHasExtension = false;
		for (size_t i = sExec.size() - 1; i > 0; i--) {
			if (sExec[i] == '/' || sExec[i] == '\\') break;
			if (sExec[i] == '.') {				
				std::string sExt = sExec.substr(i);
				if (std::find(vSupportedSuffix.begin(), vSupportedSuffix.end(), sExt) != vSupportedSuffix.end()) bHasExtension = true;
				break;
			}
		}

		if (!bHasExtension) {
			bool bFixup = false;

			for (auto const& sSuffix : vSupportedSuffix) {
				std::string sTest = sExec + sSuffix;
				if (_access(sTest.c_str(), 0) == 0) {
					sExec.append(sSuffix);
					bFixup = true;
					break;
				}
			}

			if (!bFixup) {
				LOG_ERR("Start %s ... [Failed] Path : %s, Cmd: %s, Executable NOT FOUND", sName.c_str(), pWorkDir, sCmd.c_str());
        		return;
			}
		}

		::GetFullPathNameA(sExec.c_str(), 512, pCmd, NULL);
	} else {
		strncpy(pCmd, sCmd.c_str(), 512);
	}	

    bool bOk = ::CreateProcessA(
        NULL, 
       	pCmd,
        NULL,
        NULL,
        TRUE,
        CREATE_NO_WINDOW,
        NULL,
        pWorkDir,
        &si,
        &pi);

	::CloseHandle(hWriter);

    if (!bOk) {
        ::CloseHandle(hReader);

        pInfo->emStatus = EStatus::Fatal;

        LOG_ERR("Start %s ... [Failed] Path : %s, Cmd: %s", sName.c_str(), pWorkDir, pCmd);
        return;
    }

	FILETIME ftNow;
	::GetSystemTimeAsFileTime(&ftNow);

	pInfo->hPipe = hReader;
    pInfo->nPid = pi.dwProcessId;
	pInfo->nLastCalcCPU = FileTimeToInteger(ftNow);
	pInfo->nLastCPUTotal = 0;
	pInfo->emStatus = EStatus::Running;
	pInfo->nStart = time(NULL);
	
    LOG_INFO("Start %s ... [OK]. PID : %d", sName.c_str(), pInfo->nPid);

    Json::Value iMsg;
	iMsg["action"]		= "status_change";
	iMsg["watcher"]		= sName;
	iMsg["status"]		= pInfo->emStatus;
	iMsg["pid"]			= (int)pInfo->nPid;
	iMsg["start_time"]	= pInfo->GetStartTime();

	AppendTail(sName, *pInfo, "\n\n----------- Start -----------\n");
	Notify("/ws", iMsg);
}

void Watcher::Stop(const std::string &sName) {
    auto it = _mWatcher.find(sName);
	if (it == _mWatcher.end() || it->second.emStatus != EStatus::Running) return;

    HANDLE hProc = ::OpenProcess(PROCESS_TERMINATE, FALSE, it->second.nPid);
    if (hProc == INVALID_HANDLE_VALUE) {
        LOG_ERR("Stop %s ... [Failed]", sName.c_str());
        return;
    }

    if (!::TerminateProcess(hProc, 0)) {
        ::CloseHandle(hProc);
        LOG_ERR("Stop %s ... [Failed]. Error: %d", sName.c_str(), ::GetLastError());
        return;
    }

    ::CloseHandle(hProc);
    CatchExit(sName, it->second, EStatus::Stopped);
    LOG_INFO("Stop %s ... [OK]", sName.c_str());
}

void Watcher::StopAll() {
    for (auto & kv : _mWatcher) Stop(kv.first);
}

void Watcher::Remove(const std::string &sName) {
    auto it = _mWatcher.find(sName);
	if (it == _mWatcher.end()) return;

	if (it->second.emStatus == EStatus::Running) Stop(sName);
	_mWatcher.erase(it);
}

Watcher::Info * Watcher::Get(const std::string &sName) {
    auto it = _mWatcher.find(sName);
	if (it == _mWatcher.end()) return nullptr;
	return &it->second;
}

void Watcher::Notify(const std::string & sScope, const Json::Value & rMsg) {
    if (_fNotifier) _fNotifier(sScope, rMsg);
}

void Watcher::Tail(const std::string & sName, Info & rInfo) {
    static char pLine[8192] = {0}, pData[8192] = {0};

	DWORD nSize = 0, nValid = 0;
	if (!::PeekNamedPipe(rInfo.hPipe, NULL, 0, &nSize, &nValid, NULL) || nValid <= 0) {
		return;
	}

	memset(pLine, 0, 8192);
	memset(pData, 0, 8192);

    bool bOk = ReadFile(rInfo.hPipe, pLine, 8192, &nSize, NULL);
    if (bOk && nSize > 0) {
		int nRead = 0;
		for (int i = 0; i < (int)nSize; ++i) {
			if (pLine[i] == 0x1B && pLine[i + 1] == '[') {
				while (i < (int)nSize && pLine[i] != 'm') i++;
			} else {
				pData[nRead] = pLine[i];
				++nRead;
			}
		}

		memcpy(rInfo.pTail + rInfo.nTail, pData, nRead);

		if (rInfo.nTail + nRead > 8192) {
			int nIdx = rInfo.nTail + nRead - 8192;
			int i = 0;

			for (; nIdx < rInfo.nTail + nRead; ++nIdx) {
				if (rInfo.pTail[nIdx] == '\n') {
					nIdx++;
					break;
				}
			}

			for (; i < rInfo.nTail + nRead - nIdx; ++i) {
				rInfo.pTail[i] = rInfo.pTail[i + nIdx];
			}

			rInfo.pTail[i] = '\0';
			rInfo.nTail = i;
		} else {
			rInfo.nTail += nRead;
		}

		Json::Value iMsg;
		iMsg["action"]	= "tail_append";
		iMsg["watcher"]	= sName;
		iMsg["data"]	= pData;

		Notify("/tail/" + sName, iMsg);
		Notify("/tail_all", iMsg);
	}
}

void Watcher::AppendTail(const std::string & sName, Info & rInfo, const std::string & sTail) {
    int nSize = sTail.size();
	memcpy(rInfo.pTail + rInfo.nTail, sTail.data(), nSize);

	if (rInfo.nTail + nSize > 8192) {
		int nIdx = rInfo.nTail + nSize - 8192;
		int i = 0;

		for (; nIdx < rInfo.nTail + nSize; ++nIdx) {
			if (rInfo.pTail[nIdx] == '\n') {
				nIdx++;
				break;
			}
		}

		for (; i < rInfo.nTail + nSize - nIdx; ++i) {
			rInfo.pTail[i] = rInfo.pTail[i + nIdx];
		}

		rInfo.pTail[i] = '\0';
		rInfo.nTail = i;
	} else {
		rInfo.nTail += nSize;
	}

	Json::Value iMsg;
	iMsg["action"]	= "tail_append";
	iMsg["data"]	= sTail;

	Notify("/tail/" + sName, iMsg);
}

void Watcher::CatchExit(const std::string & sName, Info & rInfo, EStatus emStatus, bool bRetry /* = false */) {
	::CloseHandle(rInfo.hPipe);

	rInfo.emStatus	= emStatus;
	rInfo.nPid		= -1;
	rInfo.nStart	= 0;

	Json::Value iMsg;
	iMsg["action"]		= "status_change";
	iMsg["watcher"]		= sName;
	iMsg["status"]		= rInfo.emStatus;
	iMsg["pid"]			= (int)rInfo.nPid;
	iMsg["start_time"]	= rInfo.GetStartTime();

	if (bRetry && rInfo.nRetry > 0) {
		rInfo.emStatus	= EStatus::Retry;
		iMsg["status"]	= EStatus::Retry;
	
		Notify("/ws", iMsg);
		AppendTail(sName, rInfo, "\n----------- Auto-retry -----------\n");

		Start(sName, rInfo.sPath, rInfo.sCmd, rInfo.nRetry - 1);
	} else {
		Notify("/ws", iMsg);
		AppendTail(sName, rInfo, "\n----------- Stopped -----------\n");
	}
}

#endif